package com.mdd.system.plugin.storage.engine;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.aliyun.oss.ClientBuilderConfiguration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.model.*;
import com.google.common.collect.Lists;
import com.mdd.common.enums.upload.FileUploadTypeEnum;
import com.mdd.common.exception.OperateException;
import com.mdd.common.util.RedisUtils;
import com.mdd.common.util.TimeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 阿里云存储
 */
public class AliyunStorage {

    /**
     * 存储配置
     */
    private final Map<String, String> config;
    private RedisUtils redisUtils=new RedisUtils();

    /**
     * 构造方法
     */
    public AliyunStorage(Map<String, String> config) {
        this.config = config;
    }

    /**
     * 鉴权令牌
     *
     * @author fzr
     * @return String
     */
    public OSS ossClient() {
            String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
            String accessKeyId = this.config.get("accessKey");
            String accessKeySecret = this.config.get("secretKey");
            System.out.println("endpoint----->" + endpoint);
            System.out.println("accessKeyId----->" + accessKeyId);
            System.out.println("accessKeySecret----->" + accessKeySecret);
            ClientBuilderConfiguration conf = new ClientBuilderConfiguration();

            // 设置OSSClient允许打开的最大HTTP连接数，默认为1024个。
            conf.setMaxConnections(200);
            // 设置Socket层传输数据的超时时间，默认为50000毫秒。
            conf.setSocketTimeout(100000);
            // 设置建立连接的超时时间，默认为50000毫秒。
            conf.setConnectionTimeout(100000);
            // 设置从连接池中获取连接的超时时间（单位：毫秒），默认不超时。
            conf.setConnectionRequestTimeout(100000);
            // 设置连接空闲超时时间。超时则关闭连接，默认为60000毫秒。
            conf.setIdleConnectionTime(100000);
            // 设置失败请求重试次数，默认为3次。
            conf.setMaxErrorRetry(5);
           return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    }

    /**
     * 上传文件
     *
     * @param multipartFile 文件对象
     * @param key 文件名称 20220331/11.png
     */
    public void upload(MultipartFile multipartFile, String key) {
        try {
            PutObjectRequest putObjectRequest = new PutObjectRequest(
                    this.config.get("bucket"), key,
                    new ByteArrayInputStream(multipartFile.getBytes())
            );
            this.ossClient().putObject(putObjectRequest);
        } catch (OSSException oe) {
            throw new OperateException(oe.getMessage());
        } catch (Exception ce) {
            throw new OperateException(ce.getMessage());
        } finally {
            if (this.ossClient() != null) {
                this.ossClient().shutdown();
            }
        }
    }
    /**
     * 分片上传
     * 1、检查文件是否上传
     * 2、检查文件是否第一次上传，第一次上传创建上传id uploadId
     * 3、检查是否是断点续传，如果是返回已上传的分片
     * 4、分片上传到阿里云OSS上，并记录上传信息到Redis
     * 5、判断是否已上传完成，已完成：合并所有分片为源文件
     *
     * @param file       文件分片
     * @param chunkIndex 分片索引
     * @param chunkSize  分片大小
     * @return
     */
    public Map uploadChunk(String fileMd5, String fileName, MultipartFile file, Integer chunkIndex,
                           Integer totalChunks,Long chunkSize) {
           String key = buildSaveName(getKey(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode(), fileMd5, FileUtil.getSuffix(fileName)));
        try {
            Map<String, Object> map = MapUtil.newHashMap();
            // 判断是否上传
            if (checkExist(fileMd5)) {
                map.put("skipUpload", true);
                return map;
            }
            String uploadId;
            Object objUploadID = RedisUtils.get(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode() + fileMd5);
            if (Objects.isNull(objUploadID)){
                // 判断是否第一次上传
                uploadId = uploadChunkInit(key);
                //保存上传ID
                RedisUtils.set(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode() + fileMd5,uploadId);
            }else {
                uploadId= (String) objUploadID;
            }
            // 上传分片
            PartETag partETag = uploadChunkPart(uploadId, key, file.getInputStream(), chunkIndex, chunkSize,fileMd5);

            // 分片上传完成缓存key
            redisUtils.lSet(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode() + fileMd5+uploadId,JSONUtil.toJsonStr(partETag));
            return map;
        } catch (Exception e) {
            e.printStackTrace();
            throw new OperateException("上传失败：" + e.getMessage());
        }

    }

    /**
     * 文件分片合并，并清理相关缓存数据
     * @param fileMd5 文件MD5(HEX)
     * @param totalChunks 总分片数
     * @param fileName 文件名称
     * @return 文件地址
     */
    public String chunkComplete(String fileMd5,Integer totalChunks,String fileName){
        String key = buildSaveName(getKey(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode(), fileMd5, FileUtil.getSuffix(fileName)));
        String uploadId;
        Object objUploadID = RedisUtils.get(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode() + fileMd5);
        Assert.notNull(objUploadID,"当前暂无上传任务不存在");
        uploadId= (String) objUploadID;
        // 取出所有已上传的分片信息
        List<Object> objects = redisUtils.lGet(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode()+fileMd5+uploadId, 0, -1);
        List<PartETag> partETagList = Lists.newArrayList();
        objects.forEach(o -> partETagList.add(JSONUtil.toBean(o.toString(), PartETag.class)));
        // 判断是否上传完成
        uploadChunkComplete(uploadId, key, partETagList,fileMd5);
        //删除缓存文件信息
        RedisUtils.del(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode()+fileMd5+uploadId);
         objects = redisUtils.lGet(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode()+fileMd5+uploadId, 0, -1);
        return key;
    }

    /**
     * 初始化上传id uploadId
     *
     * @param key
     * @return
     */
    public  String uploadChunkInit(String key) {
        if (ObjectUtil.isEmpty(key)) {
            throw new OperateException("key不能为空");
        }
        try {
            // 创建分片上传对象
            InitiateMultipartUploadRequest uploadRequest = new InitiateMultipartUploadRequest(this.config.get("bucket"), key);
            // 初始化分片
            InitiateMultipartUploadResult result = this.ossClient().initiateMultipartUpload(uploadRequest);
            // 返回uploadId，它是分片上传事件的唯一标识，您可以根据这个uploadId发起相关的操作，如取消分片上传、查询分片上传等。
            return result.getUploadId();
        } catch (Exception e) {
            e.printStackTrace();
            throw new OperateException("初始化分片失败：" + e.getMessage());
        }
    }

    /**
     * 上传分片文件
     *
     * @param uploadId   上传id
     * @param key        key
     * @param mainstream   文件分片流
     * @param chunkIndex 分片索引
     * @param chunkSize  分片大小
     * @return
     */
    public  PartETag uploadChunkPart(String uploadId, String key, InputStream mainstream,
                                     Integer chunkIndex, long chunkSize, String fileMd5) {
        try {
            UploadPartRequest partRequest = new UploadPartRequest();
            // 阿里云 oss 文件根目录
            partRequest.setBucketName(this.config.get("bucket"));
            // 文件key
            partRequest.setKey(key);
            // 分片上传uploadId
            partRequest.setUploadId(uploadId);
            // 分片文件
            partRequest.setInputStream(mainstream);
            // 分片大小。除了最后一个分片没有大小限制，其他的分片最小为100 KB。
            partRequest.setPartSize(chunkSize);
            System.out.println(chunkSize + "    " + chunkIndex + "   " + uploadId);
            // 分片号。每一个上传的分片都有一个分片号，取值范围是1~10000，如果超出这个范围，OSS将返回InvalidArgument的错误码。
            partRequest.setPartNumber(chunkIndex);
            // 每个分片不需要按顺序上传，甚至可以在不同客户端上传，OSS会按照分片号排序组成完整的文件。
            UploadPartResult uploadPartResult = this.ossClient().uploadPart(partRequest);
            // 每次上传分片之后，OSS的返回结果包含PartETag。PartETag将被保存在redis中。
            System.out.println("uploadPartResult----->"+JSONUtil.toJsonStr(uploadPartResult));
            return uploadPartResult.getPartETag();
        } catch (Exception e) {
            e.printStackTrace();
            RedisUtils.del(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode() + fileMd5);
            throw new OperateException("分片上传失败：" + e.getMessage());
        }
    }
    private String buildSaveName(String filename) {
//        String ext  = Objects.requireNonNull(filename).substring(filename.lastIndexOf("."));
        String date = TimeUtils.timestampToDate(TimeUtils.timestamp(), "yyyyMMdd");
//        return "radl/video/"+date + "/" + java.util.UUID.randomUUID() + ext.toLowerCase();
        return "radl/video/"+date+"/"+filename;
    }
    /**
     * 文件合并
     *
     * @param uploadId  上传id
     * @param key       key
     * @param chunkTags 分片上传信息
     * @return
     */
    public String uploadChunkComplete(String uploadId, String key, List<PartETag> chunkTags,String fileMd5) {
        try {
            CompleteMultipartUploadRequest completeMultipartUploadRequest =
                    new CompleteMultipartUploadRequest(this.config.get("bucket"), key, uploadId, chunkTags);
            CompleteMultipartUploadResult result = this.ossClient().completeMultipartUpload(completeMultipartUploadRequest);
            return result.getETag();
        } catch (Exception e) {
            e.printStackTrace();
            RedisUtils.del(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode()+fileMd5+uploadId);
            throw new OperateException("分片合并失败：" + e.getMessage());
        }
    }

    public  Boolean checkExist(String key) {
        return this.ossClient().doesObjectExist(this.config.get("bucket"), key);
    }

    /**
     * 获取上传文件的key
     * 上传和删除时除了需要bucketName外还需要此值
     *
     * @param prefix   前缀（非必传），可以用于区分是哪个模块或子项目上传的文件,默认 file 文件夹
     * @param fileName 文件名称（非必传），如果为空默认生成文件名，格式：yyyyMMdd-UUID
     * @param suffix   后缀 , 可以是 png jpg
     * @return
     */
    private static String getKey(final String prefix, final String fileName, final String suffix) {
        StringBuffer keySb = new StringBuffer();
        // 前缀处理
        if (StringUtils.isNotEmpty(prefix)) {
            keySb.append(prefix);
        } else {
            keySb.append(FileUploadTypeEnum.CMS_SECURITY_STUDIO.getCode());
        }
        // 文件名处理
        if (StringUtils.isBlank(fileName)) {
            // 上传时间 因为后期可能会用 - 将key进行split，然后进行分类统计
            keySb.append(DateUtil.format(LocalDateTime.now(), "yyyyMMdd"));
            keySb.append("-");
            // 生成uuid
            keySb.append(UUID.randomUUID());
        } else {
            keySb.append(fileName);
        }
        // 后缀处理
        if (StringUtils.isBlank(suffix)) {
            throw new NullPointerException("文件后缀不能为空");
        }
        if (suffix.contains(".")) {
            keySb.append(suffix.substring(suffix.lastIndexOf(".")));
        } else {
            keySb.append("." + suffix);
        }
        return keySb.toString();
    }
}
